Responder chain
めちゃくちゃわかりやすいので、以下を読んだ方が良い
概要
iOS アプリケーションは、その外界からユーザ操作として様々な種類の イベント を受け取る。シングルタップやダブルタップ、スワイプ等のジェスチャや、キーボードによる入力、ロックスクリーンや外部端末からの Remote Control 等がこのイベントに相当する。このイベントを適切に検知し、ハンドリングする必要がある。
アプリケーションがイベントを検知すると、UIKit は内部的に UIEvent ンスタンスを生成し、それをイベントキュー (UIApplication.shared.sendEvent()) に push する。その後 UIKit は UIEvent を pop し、そのイベントをハンドリング可能な responder object を決定し、イベントを送信する。ここで最初にイベントを送信される responder object が first responder と呼ばれる。 first responder を初め、responder object がイベントを受け取る場合、イベント種別に対応した I/F が呼び出される。例えば、タッチイベントであれば touchesBegan(_:with:), touchesMoved(_:with:), touchesEnded(_:with:), touchesCancelled(_:with:) などが呼び出される。これらのイベントハンドリング用の I/F のデフォルト実装は 次の responder object にイベントを forward する といったないようになっている。そのため、responder object 内でこれらを override しなかった場合、イベントが発生してもそれらが検知されることはなく、次に通知されるべき responder object に forward される。この時の responder object 間のイベントの forwarding の連なり が responder chain と呼ばれる。 code:swift
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
open func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesChanged(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionCancelled(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func remoteControlReceived(with event: UIEvent?)
発生したイベントを最初に検知するのが、そのイベントをハンドリングさせたい対象のオブジェクトであるとは限らない。基本的にはイベントは View Hierarchy の下位層から順に流れていくことになるが、表側の View でタッチイベントを受け取っても、その裏側にある別の View でイベントを受け取りたいケースも十分考えられる。 このように、どの responder object が最初にイベントをハンドリングするか? を決定するために、各 responder object はイベントを受け取った時、自身がそのイベントをハンドリングすべきかどうかを判断し、ハンドリングすべきでなければ次の responder object に受け渡していく。この時辿られる responder chain のツリーが responder chain と呼ばれ、イベントを最初に受け取る responder object のことを first responder と呼ぶ。 table:first responder
Event Type First Responder
タッチ タッチが発生したView
プレス フォーカスされているオブジェクト
シェイク 開発者ないしUIKitが指定したオブジェクト
Remote Control 開発者ないしUIKitが指定したオブジェクト
ユーザが何か操作を行いイベントを発生させた場合、
とは?
タッチイベントを保持する Responder の決定
UIKit は、どこでタッチイベントが発生したか?を決定するために、view ベースの hit-testing を利用する タッチ位置がある view の範囲外だった場合、hitTest(_:with:) メソッドはその view と全ての subview を無視する
hitTest
各 subview の point(inside:with:) メソッドを呼び出し、どの subview がタッチイベントを受け取るべきか決める
point(inside:with:) が true を返した場合、その subview の view hierarchy がさらに探索される。最終的に、もっともフロントに近く、point を含んだ view が返される 以下は無視される
アルファが 0.01 以下
ユーザインタラクションが無効化されている
hidden 状態
このメソッドは view 上のコンテンツは気にしないので、コンテンツが描画されていない場所がタッチされても反応する
code:swift
func hitTest(_ point: CGPoint,
with event: UIEvent?) -> UIView?
UIView とタッチイベント
タッチ位置が元の view から外れたとしても、view プロパティは変わらない
Responder chain の代替
これをすると、次の responder はここで返したオブジェクトになる
UIKit の多くのクラスはすでにこれをオーバーライドし、特定のオブジェクトを返している ViewController の view が window の root view であった場合、次の responder は window オブジェクトになる AppDelegate になる